跳到主要内容

TypeScript 类、接口 基本用法

类 Class

class class_name { 
// 类作用域
}

定义类的关键字为 class,后面紧跟类名,类可以包含以下几个模块(类的数据成员):

  • 字段 − 字段是类里面声明的变量。字段表示对象的有关数据。
  • 构造函数 − 类实例化时调用,可以为类的对象分配内存。
  • 方法 − 方法为对象要执行的操作。

创建类

class Car { 
// 字段
engine:string;

// 构造函数
constructor(engine:string) {
this.engine = engine
}

// 方法
disp():void {
console.log("发动机为 : "+this.engine)
}
}

? 和 ! 符号的作用

参考资料 巧用ES系列4: TypeScript中的问号 ? 与感叹号 ! 是什么意思?

什么是 ?(问号)操作符?

三元运算符

// 当 isNumber(input) 为 True 是返回 ? : 之间的部分; isNumber(input) 为 False 时
// 返回 : ; 之间的部分
const a = isNumber(input) ? input : String(input);

参数

// 这里的 ?表示这个参数 field 是一个可选参数
function getUser(user: string, field?: string) {
}

成员

// 这里的?表示这个name属性有可能不存在
class A {
name?: string
}

interface B {
name?: string
}

安全链式调用

// 这里 Error对象定义的stack是可选参数,如果这样写的话编译器会提示
// 出错 TS2532: Object is possibly 'undefined'.
return new Error().stack.split('\n');

// 我们可以添加?操作符,当stack属性存在时,调用 stack.split。若stack不存在,则返回空
return new Error().stack?.split('\n');

// 以上代码等同以下代码
const err = new Error();
return err.stack && err.stack.split('\n');

什么是!(感叹号)操作符?

一元运算符

// ! 就是将之后的结果取反,比如:
// 当 isNumber(input) 为 True 时返回 False; isNumber(input) 为 False 时返回True
const a = !isNumber(input);

成员

// 因为接口B里面name被定义为可空的值,但是实际情况是不为空的,那么我们就可以
// 通过在class里面使用!,重新强调了name这个不为空值
class A implemented B {
name!: string
}

interface B {
name?: string
}

强制链式调用

// 这里 Error对象定义的stack是可选参数,如果这样写的话编译器会提示
// 出错 TS2532: Object is possibly 'undefined'.
new Error().stack.split('\n');

// 我们确信这个字段100%出现,那么就可以添加!,强调这个字段一定存在
new Error().stack!.split('\n');

类的继承

class Shape { 
Area:number

constructor(a:number) {
this.Area = a
}
}

class Circle extends Shape {
disp():void {
console.log("圆的面积: "+this.Area)
}
}

var obj = new Circle(223);
obj.disp()

继承类的方法重写

class PrinterClass { 
doPrint():void {
console.log("父类的 doPrint() 方法。")
}
}

class StringPrinter extends PrinterClass {
doPrint():void {
super.doPrint() // 调用父类的函数
console.log("子类的 doPrint()方法。")
}
}

static 关键字

class StaticMem {  
static num:number;

static disp():void {
console.log("num 值为 "+ StaticMem.num)
}
}

StaticMem.num = 12 // 初始化静态变量
StaticMem.disp() // 调用静态方法

instanceof 运算符

instanceof 运算符用于判断对象是否是指定的类型,如果是返回 true,否则返回 false。

class Person{ } 
var obj = new Person()
var isPerson = obj instanceof Person;
console.log("obj 对象是 Person 类实例化来的吗? " + isPerson);

访问控制修饰符

TypeScript 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。TypeScript 支持 3 种不同的访问权限。

  • public(默认) : 公有,可以在任何地方被访问。
  • protected : 受保护,可以被其自身以及其子类和父类访问。
  • private : 私有,只能被其定义所在的类访问。
class Encapsulate { 
str1:string = "hello"
private str2:string = "world"
}

var obj = new Encapsulate()
console.log(obj.str1) // 可访问
console.log(obj.str2) // 编译错误, str2 是私有的

类和接口

interface ILoan { 
interest:number
}

class AgriLoan implements ILoan {
interest:number
rebate:number

constructor(interest:number,rebate:number) {
this.interest = interest
this.rebate = rebate
}
}

var obj = new AgriLoan(10,1)
console.log("利润为 : "+obj.interest+",抽成为 : "+obj.rebate )

接口 Interfaces

// 对象的结构化类型
let persion: {
name: string;
age: number;
}

// 这样赋值就会被上面的结构所约束
persion = {
name: '张三',
age: 18
}

在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。

interface IPerson { 
firstName:string,
lastName:string,
sayHi: ()=>string
}

let customer:IPerson = {
firstName:"Tom",
lastName:"Hanks",
sayHi: ():string =>{return "Hi there"}
}

接口初探

下面通过一个简单示例来观察接口是如何工作的:

function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

类型检查器会查看 printLabel 的调用。 printLabel 有一个参数,并要求这个对象参数有一个名为 label 类型为 string 的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。

下面我们重写上面的例子,这次使用接口来描述:必须包含一个 label 属性且类型为 string


interface LabelledValue {
label: string;
}

function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

LabelledValue 接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label 属性且类型为 string 的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给 printLabel 的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。

还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以

可选属性

有时我们希望不要完全匹配一个形状,那么可以用可选属性:

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ? 符号。

interface Person {
name: string;
age?: number;
}

let tom: Person = {
name: 'Tom'
};

let tom: Person = {
name: 'Tom',
age: 25
};

只读属性

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性:

interface Point {
readonly x: number;
readonly y: number;
}

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

函数类型

interface SearchFunc {
age: number;
subF(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;

mySearch = {
age: 18,
subF : function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
}

如果里面只有一个函数可以这样写:

interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;

mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}

可索引的类型

与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如 a[10]ageMap["daniel"]。 可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。

interface StringArray {
[index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

上面例子里,我们定义了 StringArray 接口,它具有索引签名。 这个索引签名表示了当用 number 去索引 StringArray 时会得到 string 类型的返回值。

实现接口

与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约。

interface ClockInterface {
currentTime: Date;
}

class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}

也可以在接口中描述一个方法,在类里实现它,如同下面的 setTime 方法一样:

interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}

class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}

Type 类型

参考资料 typescript 中的 interface 和 type 到底有什么区别?

interface 是接口,type 是类型,本身是两个概念。只是碰巧表现上比较相似。希望定义一个变量类型,就用 type,如果希望是能够继承并约束的,就用 interface。如果你不知道该用哪个,说明你只是想定义一个类型而非接口,所以应该用 type。

interface User {
name: string
age: number
}

interface SetUser {
(name: string, age: number): void;
}
type User = {
name: string
age: number
};

type SetUser = (name: string, age: number): void;

抽象类 abstract class


abstract class Animal{
public name:string;
constructor(name:string){
this.name=name;
}

//抽象方法 ,不包含具体实现,要求子类中必须实现此方法
abstract eat():any;

//非抽象方法,无需要求子类实现、重写
run(){
console.log('非抽象方法,不要子类实现、重写');
}
}


class Cat extends Animal{

//子类中必须实现父类抽象方法,否则ts编译报错
eat(){
return this.name+"吃鱼";
}
}

TypeScript 命名空间

namespace 是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间。

由于历史遗留原因,在早期还没有 ES6 的时候,ts 提供了一种模块化方案,使用 module 关键字表示内部模块。但由于后来 ES6 也使用了 module 关键字,ts 为了兼容 ES6,使用 namespace 替代了自己的 module,更名为命名空间。

随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的 namespace,而推荐使用 ES6 的模块化方案了,故我们不再需要学习 namespace 的使用了。

namespace 被淘汰了,但是在声明文件中,declare namespace 还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性。

命名空间定义了标识符的可见范围,一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的。这样,在一个新的名字空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其他名字空间中。

namespace SomeNameSpaceName { 
export interface ISomeInterfaceName { }
export class SomeClassName { }
}

以上定义了一个命名空间 SomeNameSpaceName,如果我们需要在外部可以调用 SomeNameSpaceName 中的类和接口,则需要在类和接口添加 export 关键字。

要在另外一个命名空间调用语法格式为:

SomeNameSpaceName.SomeClassName;

如果一个命名空间在一个单独的 TypeScript 文件中,则应使用三斜杠 /// 引用它,语法格式如下:

/// <reference path = "SomeFileName.ts" />

使用命名空间

以下实例演示了命名空间的使用,定义在不同文件中: IShape.ts 文件代码:

namespace Drawing { 
export interface IShape {
draw();
}
}

Circle.ts 文件代码:

/// <reference path = "IShape.ts" /> 
namespace Drawing {
export class Circle implements IShape {
public draw() {
console.log("Circle is drawn");
}
}
}

Triangle.ts 文件代码:

/// <reference path = "IShape.ts" /> 
namespace Drawing {
export class Triangle implements IShape {
public draw() {
console.log("Triangle is drawn");
}
}
}

TestShape.ts 文件代码:

/// <reference path = "IShape.ts" />   
/// <reference path = "Circle.ts" />
/// <reference path = "Triangle.ts" />
function drawAllShapes(shape:Drawing.IShape) {
shape.draw();
}
drawAllShapes(new Drawing.Circle());
drawAllShapes(new Drawing.Triangle());

使用 tsc 命令编译以上代码:

tsc --out app.js TestShape.ts  

使用 node 命令查看输出结果为:

node app.js
-> Circle is drawn
-> Triangle is drawn

嵌套命名空间

命名空间支持嵌套,即你可以将命名空间定义在另外一个命名空间里头。

namespace namespace_name1 { 
export namespace namespace_name2 {
export class class_name { }
}
}

成员的访问使用点号 . 来实现,如下实例: Invoice.ts 文件代码:

namespace Runoob { 
export namespace invoiceApp {
export class Invoice {
public calculateDiscount(price: number) {
return price * .40;
}
}
}
}

InvoiceTest.ts 文件代码:

/// <reference path = "Invoice.ts" />
var invoice = new Runoob.invoiceApp.Invoice();
console.log(invoice.calculateDiscount(500));

使用 tsc 命令编译以上代码:

tsc --out app.js InvoiceTest.ts